package cz.muni.fi.pa165.sportclubmanager.service;

import cz.muni.fi.pa165.sportclubmanager.dao.AgeCategoryDAO;
import cz.muni.fi.pa165.sportclubmanager.dao.PlayerDAO;
import cz.muni.fi.pa165.sportclubmanager.dao.PlayerOnRosterDAO;
import cz.muni.fi.pa165.sportclubmanager.dao.TeamDAO;
import cz.muni.fi.pa165.sportclubmanager.dto.PlayerDTO;
import cz.muni.fi.pa165.sportclubmanager.dto.PlayerDTOMapping;
import cz.muni.fi.pa165.sportclubmanager.dto.RosterDTO;
import cz.muni.fi.pa165.sportclubmanager.dto.RosterDTOMapping;
import cz.muni.fi.pa165.sportclubmanager.dto.TeamDTO;
import cz.muni.fi.pa165.sportclubmanager.dto.TeamDTOMapping;
import cz.muni.fi.pa165.sportclubmanager.entity.AgeCategory;
import cz.muni.fi.pa165.sportclubmanager.entity.Player;
import cz.muni.fi.pa165.sportclubmanager.entity.PlayerOnRoster;
import cz.muni.fi.pa165.sportclubmanager.entity.Team;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 *
 * @author Michal Prokeš, 72758
 */
@Service
public class RosterServiceImpl implements RosterService {

    public static final int NUMBER_MIN_VALUE = 1;
    public static final int NUMBER_MAX_VALUE = 99;
    public static final int AGE_TOO_OLD = 1;
    public static final int AGE_OK = 0;
    public static final int AGE_TOO_YOUNG = -1;
    private AgeCategoryDAO acDAO;

    @Autowired
    public void setAgeCategoryDAO(AgeCategoryDAO acDAO) {
        this.acDAO = acDAO;
    }
    private PlayerOnRosterDAO rosterDAO;

    @Autowired
    public void setRosterDAO(PlayerOnRosterDAO rosterDAO) {
        this.rosterDAO = rosterDAO;
    }
    private TeamDAO teamDAO;

    @Autowired
    public void setTeamDAO(TeamDAO teamDAO) {
        this.teamDAO = teamDAO;
    }
    private PlayerDAO playerDAO;

    @Autowired
    public void setPlayerDAO(PlayerDAO playerDAO) {
        this.playerDAO = playerDAO;
    }

    private boolean isProper(int n) {
        return n >= NUMBER_MIN_VALUE && n <= NUMBER_MAX_VALUE;
    }

    private int checkAgeStatus(AgeCategory ac, Date birthDate) {

        AgeCategory acLower = acDAO.findClosestLower(ac.getId());
        if (acLower == null) {
            acLower = ac;
        }


        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
        int yearFrom = currentYear - ac.getAgeTo();
        int yearTo = currentYear - acLower.getAgeFrom();

        Date dateFrom = new GregorianCalendar(yearFrom, 0, 1).getTime();
        Date dateTo = new GregorianCalendar(yearTo, 11, 31).getTime();

        if (birthDate.compareTo(dateFrom) < 0) {
            return AGE_TOO_OLD;
        }
        if (birthDate.compareTo(dateTo) > 0) {
            return AGE_TOO_YOUNG;
        }
        return AGE_OK;
    }

    @Transactional
    @Override    
    @Secured({"ROLE_ADMIN"})
    public void addPlayerToRoster(TeamDTO t, PlayerDTO p, int n) {


        // check number is in range
        if (!isProper(n)) {
            throw new RosterException("Chosen number isn't in permitted range <" + NUMBER_MIN_VALUE + "," + NUMBER_MAX_VALUE + ">: " + n);
        }

        Team team = teamDAO.find(t.getId());
        Player player = playerDAO.find(p.getId());
        List<Team> teamsInAC = teamDAO.findByAgeCategory(team.getAgeCategory());
        List<PlayerOnRoster> playersAssignments = rosterDAO.findAllWithPlayer(player);

        // can play in agecategory of this team or category one step below?
        int ageStatus = checkAgeStatus(team.getAgeCategory(), p.getBirthDate());

        if (ageStatus != AGE_OK) {
            throw new RosterException("Player " + player.getFirstName() + " " + player.getLastName()
                    + " is too " + (ageStatus == AGE_TOO_YOUNG ? "young" : "old") + " for team " + team.getName());
        }



        // not in team already and not in other team in the same age category - the second condition covers the first one
        for (PlayerOnRoster por : playersAssignments) {
            if ((por.getTeam().getAgeCategory().equals(team.getAgeCategory()))) {
                // found assignment in the same age category, which is bad already, so only distinct between this team and other team
                String msg = "";
                if (teamsInAC.contains(por.getTeam())) {
                    msg = "Player is already on roster.";
                } else {
                    msg = "Player is already on roster of other team within the same ageCategory.";
                }
                throw new RosterException(msg);

            } else {
                continue; // skip check for non-relevant age category
            }
        }

        // check unique number on roster
        List<PlayerOnRoster> roster = rosterDAO.findAllWithTeam(team);
        for (PlayerOnRoster por : roster) {
            if (por.getNumber() == n) {
                throw new RosterException("Number " + n + " is already assigned to other player.");
            }
        }

        // all checks pass, OK
        PlayerOnRoster playerOnRoster = new PlayerOnRoster();
        playerOnRoster.setNumber(n);
        playerOnRoster.setTeam(team);
        playerOnRoster.setPlayer(player);

        rosterDAO.create(playerOnRoster);
    }

    @Override
    @Transactional   
    @Secured({"ROLE_ADMIN"})
    public void removePlayerFromRoster(RosterDTO roster) {

        // get entity
        PlayerOnRoster por = rosterDAO.find(roster.getId());

        // if present, remove it
        if (por != null) {
            rosterDAO.remove(por);
        }
    }

    @Override
    @Transactional
    @Secured({"ROLE_ADMIN"})    
    public void changeNumber(RosterDTO roster) {
        // check number is in range
        if (!isProper(roster.getNumber())) {
            throw new RosterException("Chosen number isn't in permitted range <" + NUMBER_MIN_VALUE + "," + NUMBER_MAX_VALUE + ">: " + roster.getNumber());
        }

        // get entity with given id and change only number
        PlayerOnRoster porNew = rosterDAO.find(roster.getId());
        porNew.setNumber(roster.getNumber());

        // check unique number on roster
        List<PlayerOnRoster> pors = rosterDAO.findAllWithTeam(porNew.getTeam());
        for (PlayerOnRoster por : pors) {
            // skip the updated relation to be able to update number
            if (por.equals(porNew)) {
                continue;
            }
            if (por.getNumber() == porNew.getNumber()) {
                throw new RosterException("Number " + porNew.getNumber() + " is already assigned to other player.");
            }
        }

        // all ok, update it
        rosterDAO.update(porNew);
    }

    @Override
    @Transactional(readOnly = true)    
    @Secured({"ROLE_USER","ROLE_ADMIN"})        
    public List<RosterDTO> getPlayersOnRoster(TeamDTO t) {
        List<PlayerOnRoster> list = rosterDAO.findAllWithTeam(TeamDTOMapping.toEntity(t));
        List<RosterDTO> dtoList = new ArrayList<RosterDTO>();

        for (PlayerOnRoster por : list) {
            dtoList.add(RosterDTOMapping.toDTO(por));
        }
        return dtoList;
    }

    @Override
    @Transactional(readOnly = true)    
    @Secured({"ROLE_USER","ROLE_ADMIN"})        
    public List<PlayerDTO> getPlayersAvailableForRoster(TeamDTO t) {
        
        Team team = teamDAO.find(t.getId());
        
        // get all teams for this ac
        List<Team> teams = teamDAO.findByAgeCategory(team.getAgeCategory());
        
        // get all players for this ac
        Set<Player> players = new HashSet<Player>(playerDAO.getPlayersFor(team.getAgeCategory()));
        
        // get lower ac
        AgeCategory aclower = acDAO.findClosestLower(team.getAgeCategory().getId());
        
        if (aclower != null) {
            List<Player> playersInLowerAc = playerDAO.getPlayersFor(aclower);

            // merge both collections
            players.addAll(playersInLowerAc);
        }
        
        // iterate trough all teams and filter out already assigned players
        for (Team team1 : teams) {
            List<PlayerOnRoster> findAllWithTeam = rosterDAO.findAllWithTeam(team1);
            for (PlayerOnRoster por : findAllWithTeam) {
                players.remove(por.getPlayer());
            }            
        }
        
        List<Player> playersList = new ArrayList<Player>(players);
        
        // sort by lastname, firstname
        Collections.sort(playersList, new Comparator<Player>() {
            @Override
            public int compare(Player o1, Player o2) {
                if (o1 == null) {
                    return -1;
                }
                if (o2 == null) {
                    return 1;
                }

                return o1.getLastName().equals(o2.getLastName())
                        ? o1.getLastName().compareTo(o2.getLastName())
                        : o1.getFirstName().compareTo(o2.getFirstName());
            }
        });

        // return list of dtos
        List<PlayerDTO> dtoList = new ArrayList<PlayerDTO>();
        for (Player p : playersList) {
            dtoList.add(PlayerDTOMapping.toDto(p));
        }
        return dtoList;
    }

    @Override
    @Transactional(readOnly = true)    
    @Secured({"ROLE_USER","ROLE_ADMIN"})        
    public List<TeamDTO> getTeamsWithPlayer(PlayerDTO p) {
        List<PlayerOnRoster> list = rosterDAO.findAllWithPlayer(PlayerDTOMapping.toEntity(p));
        List<TeamDTO> dtoList = new ArrayList<TeamDTO>();

        for (PlayerOnRoster por : list) {
            dtoList.add(TeamDTOMapping.toDto(por.getTeam()));
        }
        return dtoList;
    }
}
